Skip to content

Add public contact endpoint delivering via Resend - #18

Merged
amrtgaber merged 1 commit into
mainfrom
contact-endpoint
Jul 21, 2026
Merged

Add public contact endpoint delivering via Resend#18
amrtgaber merged 1 commit into
mainfrom
contact-endpoint

Conversation

@amrtgaber

Copy link
Copy Markdown
Contributor

Summary

The site's contact form had no backend — the frontend faked success. This adds the real endpoint.

  • POST /contact (public, no auth): validates a ContactCreate payload (name/email/subject/message with length limits) and emails it to the site inbox via the Resend HTTP API. From stays on the verified sending domain; Reply-To is the submitter, so replies go straight back.
  • Honeypot: a website field humans never see; when non-empty the endpoint returns 202 but drops the message, giving bots no signal.
  • Failure modes are explicit: 503 while RESEND_API_KEY/CONTACT_TO_EMAIL are unconfigured, 502 when the provider rejects a send (transport errors and non-2xx wrapped in EmailDeliveryError, logged with truncated response bodies).
  • Rate limiting: the existing auth rate-limit middleware generalized to a path-keyed table; /contact capped at 3/minute per client IP.
  • Settings gain resend_api_key, contact_to_email, contact_from_email; the deploy workflow maps the two secrets into Cloud Run. The runtime service account was granted secretmanager.secretAccessor on both (done out-of-band).
  • httpx promoted from dev to runtime dependency.

Testing

  • Six new tests: success, honeypot short-circuit, unconfigured 503, provider-failure 502, validation 422s, and the 3/minute rate limit. Full suite: 30 passed; ruff check/format clean.
  • Verified end-to-end locally with the production Resend key against Resend's test inbox (delivered@resend.dev): real send accepted with 202, both from curl and through the site's form in a browser.

The site's contact form had no backend; this adds POST /contact.

- ContactCreate schema with length/email validation and a honeypot
  field (website) — honeypot submissions return 202 but are dropped
- app/email.py sends via the Resend HTTP API: From on the verified
  sending domain, Reply-To set to the submitter, 10s timeout; transport
  errors and non-2xx responses raise EmailDeliveryError
- Router returns 503 while RESEND_API_KEY/CONTACT_TO_EMAIL are unset,
  502 when the provider rejects a send
- Rate limited at 3/minute by generalizing the auth rate-limit
  middleware to a path-keyed table (_PATH_RATE_LIMITS)
- Settings: resend_api_key, contact_to_email, contact_from_email
- Deploy maps RESEND_API_KEY and CONTACT_TO_EMAIL secrets into Cloud
  Run (runtime SA granted secretAccessor on both out-of-band)
- httpx promoted to a runtime dependency; six new tests cover success,
  honeypot, unconfigured, provider failure, validation, and rate limit
@amrtgaber
amrtgaber merged commit f5cbc1e into main Jul 21, 2026
2 checks passed
@amrtgaber
amrtgaber deleted the contact-endpoint branch July 21, 2026 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant